package com.cfw.util;

import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.cfw.collection.MapTree;

/**
 * history:
 * 	2013.9.26 - documentToMap, documentToTree, nodeToMapByPar, nodeToTreeByPar, nodeListToMap, nodeListToTree synchronized 적용
 */
final public class XmlUtil {
	private static final Logger logger = LoggerFactory.getLogger(XmlUtil.class);
	
	final private static String MSG_PAR_TAG_NULL =	"매개변수 tag문자열이  null입니다.";
	final private static String MSG_USE_ONLY_MAP =	"Map에 대한 처리만 가능합니다.";
	
	final public static String EM_TEXT =		"#text";
	final public static String EM_CDATA =		"#cdata-section";
	final public static String EM_COMMENT = 	"#comment";
	final public static String ID_NAME =		"_name";
	final public static String ID_VALUE =		"_value";
	final public static String ID_TEXT =		"_text";
	final public static String LS_CHILD =		"child";
	
	private XmlUtil() { };
	
		/**
		 * node에 포함된 attribute들을 map에 key:attribute.getNodeName, value:attribute.getNodeValue로 넣는다.
		 * @param node
		 * @param map
		 */
		private static void addAttToMap(final Node node, final Map<String,Object> map) {
			final NamedNodeMap attMap =	node.getAttributes();
			
			for (int j = 0; attMap != null && j < attMap.getLength(); j++) {
				final Attr att =	(Attr)attMap.item(j);
				map.put(att.getNodeName(), att.getNodeValue());
			}
		}
	
	/**
	 * Document에 포함된 모든 node를 Map<String,Object>으로 변환하여 반환한다. Map에서 하위 node들은 key:'child', value:List로 처리한다.<br/>
	 * Document.getElementsByTagName("*")에서 반환되는 NodeList는 계층관계를 가지고 있지 않기 때문에, 
	 * node에 대한 reference를 locateMap에 할당하고, 계층구조를 파악하기 위해 사용한다.
	 * NOTE: 다수 thread 수행시 Nullpoint Exception 발생하여 synchronized 적용 
	 * @param document
	 * @return
	 */
	public static synchronized Map<String,Object> documentToMap(final Document document) {
		// parnetMap에는 loop 중의 모든 node에 대해 대응되는 map이 put된다.
		// 계층구조상에 ArrayList가 put되어야 할 parent map을 찾기 위해서 사용된다.
		HashMap<String,Object> result =						null;
		final HashMap<Node,Map<String,Object>> locateMap =	new HashMap<Node,Map<String,Object>>();
		final NodeList nodelist =							document.getElementsByTagName("*");
		
		for (int i = 0; nodelist != null && i < nodelist.getLength(); i++) {
			final Node node =	nodelist.item(i);

			// Map을 생성하고 node의 정보를 entry로 put
			final HashMap<String,Object> map =	new HashMap<String,Object>();
			
			String nodeName =	"";
			String nodeValue =	"";
			
			nodeName = 	node.getNodeName();
			nodeValue =	getNodeValue(node);
			
			addAttToMap(node, map);

			final NodeList childNodeList =	node.getChildNodes();

			map.put(ID_NAME, 	nodeName);
			map.put(ID_VALUE, 	nodeValue);
			
			// getElementsByTagName("*")로 반환된 결과에는 #text, #cdata-section node는 포함되지 않아 
			// child node를 조회해야 한다.
			// 그리고, text, cdadta section을 둘다 지정할 경우는 1값만 유지된다.
			for (int j = 0; j < childNodeList.getLength(); j++) {
				final Node childNode =	childNodeList.item(j);
				
				if (EM_TEXT.equals(childNode.getNodeName()) || EM_CDATA.equals(childNode.getNodeName())) {
					final String value = childNode.getNodeValue().trim();

					if (value != null && !"".equals(value)) {
						map.put(ID_TEXT, value);
					}
				}
			}
			
			final Node parentNode =	node.getParentNode();
			
			// parent map을 구한다.
			final HashMap<String,Object> parentMap =	(HashMap<String,Object>)locateMap.get(parentNode);

			// parentNode에 대한 Map이 등록되어 있지 않은 경우 (최초 loop)
			if (parentMap == null) {
				result =	map;
			} else {
				// parent map의 'child' List을 구한다. 'child' List에 현재의 map을 추가한다.
				List<Map<String,Object>> list =	(List<Map<String,Object>>)parentMap.get(LS_CHILD);
				
				// 'child' List가 생성된 상태가 아니라면 생성한다.
				if (list == null) {
					list = new ArrayList<Map<String,Object>>();
					parentMap.put(LS_CHILD, list);
				}
				
				list.add(map);
			}
			
			locateMap.put(node, map);
		}
		return result;
	}
	
	/**
	 * Document에 포함된 모든 node를 MapTree로 변환하여 반환한다.
	 * Document.getElementsByTagName("*")에서 반환되는 NodeList는 계층관계를 가지고 있지 않기 때문에, 
	 * node에 대한 reference를 parentMap에 할당하고, 계층구조를 파악하기 위해 사용한다.
	 * @param document
	 * @return
	 */
	public synchronized static MapTree documentToTree(final Document document) {
		final MapTree result =		new MapTree();
		final HashMap<Node,MapTree> parentMap =	
									new HashMap<Node,MapTree>();
		final NodeList nodelist =	document.getElementsByTagName("*");
		
		for (int i = 0; nodelist != null && i < nodelist.getLength(); i++) {
			final Node node =	nodelist.item(i);

			final HashMap<String,Object> map =	new HashMap<String,Object>();
			
			String nodeName =	"";
			String nodeValue =	"";
			
			nodeName = 	node.getNodeName();
			nodeValue =	getNodeValue(node);

			addAttToMap(node, map);

			final NodeList childNodeList =	node.getChildNodes();
			
			for (int j = 0; j < childNodeList.getLength(); j++) {
				final Node cnd =	childNodeList.item(j);
				
				if (EM_TEXT.equals(cnd.getNodeName()) || EM_CDATA.equals(cnd.getNodeName())) {
					final String value = cnd.getNodeValue().trim();

					if (value != null && !"".equals(value)) {
						map.put(ID_TEXT, value);
					}
				}
			}

			map.put(ID_NAME, 	nodeName);
			map.put(ID_VALUE, 	nodeValue);

			final Node parentNode =	node.getParentNode();
			
			// parent map을 찾고, 현재 map을 추가하도록 한다.
			final MapTree parent =	(MapTree)parentMap.get(parentNode);
			MapTree current =	null;
			
			// parent가 null인 경우는 최초 최상위 node의 상위 node에 대하여 검색한 결과이며, 이 경우 #document node가 반환된다.
			// 그 외의 경우는 parent는 null이 아니다.
			// 이 경우 MapTree의 root로 지정한다.
			if (parent == null) {
				result.setUserObject(map);
				current =	result;
			} else {
				current =	parent.add(map);
			}
			
			// 상위node의 검색을 위하여 parentMap에 할당한다.
			parentMap.put(node, current);
			
		}
		return result;
	}	

		/**
		 * Node를 Map을 변환한다.<br/>
		 * Node에 포함된 하위 node들은 List형태로 변환된다. 
		 * @param node
		 * @param parentMap
		 */
		private synchronized static void nodeToMapByPar(final Node node, final Map<String,Object> parentMap) {
			String nodeName =	"";
			String nodeValue =	"";
			
			nodeName =	node.getNodeName();
			nodeValue =	getNodeValue(node);
	
			if ((EM_TEXT.equals(nodeName) || EM_CDATA.equals(nodeName)) && "".equals(nodeValue) && !node.hasAttributes() && !node.hasChildNodes()) {
				return;
			}
			
			if (EM_COMMENT.equals(nodeName)) {
				return;
			}
			
			if (EM_TEXT.equals(nodeName) || EM_CDATA.equals(nodeName)) {
				parentMap.put(ID_TEXT, nodeValue);
			} else {
				// parentMap이 isEmpty인 경우는 최초로 수행될 경우, 그 외의 경우는 ID_NAME, ID_VALUE key를 가지는 entry를 가지고 있다
				Map<String,Object> current =	null;
				
				if (parentMap.isEmpty()) {
					current =	parentMap;
				} else {
					ArrayList<Map<String,Object>> childList = (ArrayList<Map<String,Object>>)parentMap.get(LS_CHILD);
					
					if (childList == null) {
						childList =	new ArrayList<Map<String,Object>>();
						parentMap.put(LS_CHILD, childList);
					}

					// parentMap의 child List에 추가한다.
					current =	new HashMap<String,Object>();
					childList.add(current);
				}

				current.put(ID_NAME,	nodeName);
				current.put(ID_VALUE,	nodeValue);

				addAttToMap(node, current);
				
				for (int j = 0; j < node.getChildNodes().getLength(); j++) {
					nodeToMapByPar(node.getChildNodes().item(j), current);
				}
			}
		}	
	
	/**
	 * Node를 Map을 변환한다.<br/>
	 * Node에 포함된 하위 node들은 List형태로 변환된다.
	 * @param node
	 * @return
	 */
	public static Map<String,Object> nodeToMap(final Node node) {
		final HashMap<String,Object> map =	new HashMap<String,Object>();
		
		nodeToMapByPar(node, map);
		
		return map;
	}
	
		/**
		 * Node를 MapTree로 변환한다.<br/>
		 * Node에 포함된 하위 node들은 List형태로 변환된다.<br/>
		 * @param node
		 * @param tree
		 * @param scanChild
		 */
		private synchronized static void nodeToTreeByPar(final Node node, final MapTree parentTree) {
			String nodeName =	"";
			String nodeValue =	"";
			
			nodeName =	node.getNodeName();
			nodeValue =	getNodeValue(node);
	
			if ((EM_TEXT.equals(nodeName) || EM_CDATA.equals(nodeName)) && "".equals(nodeValue) && !node.hasAttributes() && !node.hasChildNodes()) {
				return;
			}
			
			if (EM_COMMENT.equals(nodeName)) {
				return;
			}
			
			// parentMap이 isEmpty인 경우는 최초로 수행될 경우, 그 외의 경우는 ID_NAME, ID_VALUE key를 가지는 entry를 가지고 있다
			if (EM_TEXT.equals(nodeName) || EM_CDATA.equals(nodeName)) {
				final Map<String,Object> map =	(Map<String,Object>)parentTree.getUserObject();
				map.put(ID_TEXT, nodeValue);
			} else {
				final HashMap<String,Object> map =	new HashMap<String,Object> ();
				
				map.put(ID_NAME,	nodeName);
				map.put(ID_VALUE,	nodeValue);
		
				addAttToMap(node, map);
			
				// node가 root노드인 경우는 tree에 node가 map으로 변환되어 추가되고, root노드가 아닌경우는 
				// tree에 node가 map으로 변환되어 하위노드로 추가된다.
				// > tree에 user object가 할당되지 않았을 경우를 root node에 대한 처리가 아직 수행되지 않았음으로 
				// > 판단하고 현재 node를 root node로 할당한다.
				MapTree current =	null;
				if (parentTree.getUserObject() == null) {
					parentTree.setUserObject(map);
					current =	parentTree;
				} else {
					current =	parentTree.add(map);
				}
				
				if (node.hasChildNodes()) {
					for (int j = 0; j < node.getChildNodes().getLength(); j++) {
						nodeToTreeByPar(node.getChildNodes().item(j), current);
					}
				}
			}
		}
		
	/**
	 * Node를 MapTree로 변환한다.<br/>
	 * Node에 포함된 하위 node들은 List형태로 변환된다.<br/>
	 * @param node
	 * @return
	 */
	public static MapTree nodeToTree(final Node node) {
		final MapTree tree =	new MapTree();
		nodeToTreeByPar(node, tree);
		
		return tree;
	}
	
	/**
	 * nodelist를 Map으로 변환한다.
	 * 매개변수가 nodelist일 경우, 계층관계는 보증할 수 없으므로  (nodelist에 포함된 node가 상위node로 nodelist에 포함되지 않은 node를 포함할 수 있다)
	 * 하위노드에 대한 처리는 수행하지 않고 단순히 List에 add한다.
	 * @param nodelist
	 * @return
	 */
	public synchronized static Map<String,Object> nodeListToMap(final NodeList nodelist) {
		final HashMap<String,Object> root =			new HashMap<String,Object>();
		final ArrayList<Map<String,Object>> list =	new ArrayList<Map<String,Object>>();
		root.put("root", list);
		
		for (int i = 0; i < nodelist.getLength(); i++) {
			final Node node =			nodelist.item(i);
			final String nodeName =		node.getNodeName();
			final String nodeValue =	getNodeValue(node);
			
			if ((EM_TEXT.equals(nodeName) || EM_CDATA.equals(nodeName) || EM_COMMENT.equals(nodeName))) {
				continue;
			}
			
			final HashMap<String,Object> map =	new HashMap<String,Object> ();
			
			map.put(ID_NAME,	nodeName);
			map.put(ID_VALUE,	nodeValue);
	
			addAttToMap(node, map);
			
			final NodeList childNodeList =	node.getChildNodes();
			
			for (int j = 0; j < childNodeList.getLength(); j++) {
				final Node cnd =	childNodeList.item(j);
				if (EM_TEXT.equals(cnd.getNodeName()) || EM_CDATA.equals(cnd.getNodeName())) {
					final String value = cnd.getNodeValue().trim();

					if (value != null && !"".equals(value)) {
						map.put(ID_TEXT, value);
					}
				}
			}
			
			list.add(map);
		}
		
		return root;
	}
	
	/**
	 * nodelist를 MapTree로 변환한다.
	 * 매개변수가 nodelist일 경우, 계층관계는 보증할 수 없으므로  (nodelist에 포함된 node가 상위node로 nodelist에 포함되지 않은 node를 포함할 수 있다)
	 * 하위노드에 대한 처리는 수행하지 않고 신규생성한 MapTree root node의 하위node로 add한다.
	 * @param nodelist
	 * @return
	 */
	public synchronized static MapTree nodeListToTree(final NodeList nodelist) {
		final MapTree tree =	new MapTree();
		
		for (int i = 0; i < nodelist.getLength(); i++) {
			final Node node =			nodelist.item(i);
			final String nodeName =		node.getNodeName();
			final String nodeValue =	getNodeValue(node);
			
			if ((EM_TEXT.equals(nodeName) || EM_CDATA.equals(nodeName) || EM_COMMENT.equals(nodeName))) {
				continue;
			}
			
			final HashMap<String,Object> map =	new HashMap<String,Object> ();
			
			map.put(ID_NAME,	nodeName);
			map.put(ID_VALUE,	nodeValue);
	
			addAttToMap(node, map);
			
			final NodeList childNodeList =	node.getChildNodes();
			
			for (int j = 0; j < childNodeList.getLength(); j++) {
				final Node cnd =	childNodeList.item(j);
				if (EM_TEXT.equals(cnd.getNodeName()) || EM_CDATA.equals(cnd.getNodeName())) {
					final String value = cnd.getNodeValue().trim();

					if (value != null && !"".equals(value)) {
						map.put(ID_TEXT, value);
					}
				}
			}
			
			tree.add(map);
		}
		
		return tree;
	}
	
	/**
	 * MapTree tree에서 userObject로 가지고 있는 Map에 대하여, _name을 key로 갖는 Map을 검색하여 반환
	 * > tree가 Map으로 구성되어있는 일반적이지 않은 경우이므로  이 method는 MapTree Class에 포함하지 않고, XmlUtil Class에 포함한다.
	 * @param tree
	 * @param name
	 * @return
	 */
	public static MapTree searchByName(final MapTree tree, final String name) {
		final Enumeration allnode =	tree.depthFirstEnumeration();
		MapTree result =			null;
		
		while (allnode.hasMoreElements()) {
			final MapTree eachnode =	(MapTree)allnode.nextElement();
			
			final Object object =		eachnode.getUserObject();
			if (object instanceof Map) {
				final String objectName =	(String)((Map)object).get(ID_NAME);
				if (objectName.equals(name)) {
					result =	eachnode;
					break;
				}
			}
		}
		return result;
	}

	/**
	 * MapTree tree에서 userObject로 가지고 있는 Map의 _text를 key로 갖는 entry의 값을 반환
	 * @param tree
	 * @return
	 */
	public static String getTreeText(final MapTree tree) {
		String result =	"";
		
		if (tree == null) {
			throw new IllegalArgumentException(XmlUtil.MSG_PAR_TAG_NULL);
		} 
		
		final Object object =	tree.getUserObject();
		if (object instanceof Map) {
			result =	(String)((Map)object).get(ID_TEXT);
		} else {
			throw new IllegalArgumentException(XmlUtil.MSG_USE_ONLY_MAP);
		}
		return result;
	}
	
	public static String getNotEmptyText(final Node node) {
		String result = null;
		
		if (node != null) {
			for (int i = 0; i < node.getChildNodes().getLength(); i++) {
				Node child = node.getChildNodes().item(i);
				if (child.getNodeType() == Node.TEXT_NODE && StringUtils.isNotEmpty(StringUtils.trim(child.getNodeValue()))) {
					result = child.getNodeValue();
					break;
				}
			}
		}
		
		return result;
	}
	
	public static List<String> getNotEmptyNodeListText(final NodeList list) {
		List<String> result = new ArrayList<String>();
		
		if (list != null) {
			for (int i = 0; i < list.getLength(); i++) {
				result.add(getNotEmptyText(list.item(i)));
			}
		}
		
		return result;
	}
	
	/**
	 * node.getNodeValue를 반환한다.
	 * node == null, node.getNodeValue == null일 경우 ""로 처리, 
	 * node.getNodeValue != null 인 경우 trim()을 수행한 결과값을 반환한다.
	 * @param node
	 * @return
	 */
	public static String getNodeValue(final Node node) {
		String result =	"";
		
		if (node != null) {
			if (StringUtils.isNotEmpty(node.getNodeValue())) {
				result = StringUtils.trim(node.getNodeValue());
			}
		}
		
		return result;
	}
	
	public static String getXPathNodeValue(final XPath xpath, final String exp, final Node node) throws XPathExpressionException {
		return getNodeValue((Node)xpath.evaluate(exp, node, XPathConstants.NODE));
	}
	
	public static List<String> getNodeValues(final NodeList list) {
		List<String> result = new ArrayList<String>();
		
		if (list != null) {
			for (int i = 0; i < list.getLength(); i++) {
				result.add(list.item(i).getNodeValue());
			}
		}
		
		return result;
	}

	public static List<String> getXPathNodeValues(final XPath xpath, final String exp, final Node node) throws XPathExpressionException {
		return getNodeValues((NodeList)xpath.evaluate(exp, node, XPathConstants.NODESET));
	}
	
	public static NodeList getXPathNodeList(final XPath xpath, final String exp, final Node node) throws XPathExpressionException {
		return (NodeList)xpath.evaluate(exp, node, XPathConstants.NODESET);
	}
	
	public static Node getXPathNode(final XPath xpath, final String exp, final Node node) throws XPathExpressionException {
		return (Node)xpath.evaluate(exp, node, XPathConstants.NODE);
	}
	
	public static String getNodePath(final Node node) {
		Node ref = node;
		StringBuilder builder = new StringBuilder();
		
		while (ref != null) {
			builder.insert(0, ref.getNodeName()).insert(0, '/');
			ref = ref.getParentNode();
		}
		
		return builder.toString();
	}
}
